Video Thumbnail
3:41
2:56
clock icon Created with Sketch. 3 minutes

Solution: DRY


Arman

Hi all! My variant

class OrderType(StrEnum):
ONLINE = "online"
IN_STORE = "in-store"

class EmailType(StrEnum):
CONFIRMATION = auto()
SHIPPING = auto()

@dataclass
class Item:
name: str
price: Decimal

@dataclass
class Order:
id: int
type: OrderType
customer_email: str

@dataclass
class Email:
body: str
subject: str
recipient: str
sender: str

def calculate_total_price(items: Iterable[Item]) -> Decimal:
return Decimal(sum(item.price for item in items))

def calculate_discounted_price(items: Iterable[Item], discount: Decimal) -> Decimal:
return calculate_total_price(items) * (1 - discount)

def generate_email(order: Order, email_type: EmailType) -> Email:
if email_type == EmailType.CONFIRMATION:
body = f"Thank you for your order! Your order #{order.id} has been confirmed."
subject = "Order Confirmation"
elif email_type == EmailType.SHIPPING:
body = f"Good news! Your order #{order.id} has been shipped and is on its way."
subject = "Order Shipped"
else:
raise ValueError(f"Unknown email type: {email_type}")

return Email(
body=body,
subject=subject,
recipient=order.customer_email,
sender="sales@webshop.com",
)

def process_order(order: Order) -> None:
print(f"Processing {order.type} order...")
print(generate_email(order, EmailType.CONFIRMATION))

if order.type == OrderType.ONLINE:
print("Shipping the order...")
print(generate_email(order, EmailType.SHIPPING))
elif order.type == OrderType.IN_STORE:
print("Order ready for pickup.")
else:
raise ValueError(f"Unknown order type: {order.type}")

print("Order processed successfully.")

def main() -> None:
items = [
Item(name="T-Shirt", price=Decimal("19.99")),
Item(name="Jeans", price=Decimal("49.99")),
Item(name="Shoes", price=Decimal("79.99")),
]

online_order = Order(
id=123, type=OrderType.ONLINE, customer_email="sarah@gmail.com"
)

total_price = calculate_total_price(items)
print("Total price:", total_price)

discounted_price = calculate_discounted_price(items, Decimal("0.1"))
print("Discounted price:", discounted_price)

process_order(online_order)

in_store_order = Order(
id=456, type=OrderType.IN_STORE, customer_email="john@gmail.com"
)

process_order(in_store_order)

REPLY
Andreas [ArjanCodes Team]

Nice solution, Arman! It looks like you are following the DRY principle. I would, however, like to give you a challenge: if you can, try to remove the if-elif-else statement from generate_email and see if you can separate them a bit more nicely

REPLY
Jacky Wong

Watching the challenges and reading the solutions people came up with in the comments has been fascinating and inspiring. A suggestion for the site is to support markdown and provide syntax highlighting in the code block.

REPLY
Arjan Egges

Hi Jacky, glad to hear you enjoy the challenges! Yes, I agree it would be great if the comment section would support Markdown and syntax highlighting. Unfortunately, it seems it's not a priority for the course platform that I use for hosting the courses as I've requested this feature already quite a while ago.

REPLY
Paul Devey

Well, I didn't quite follow directions. I did a combination between reducing repetition and changing the scope of the functions.

from dataclasses import dataclass
from decimal import Decimal
from enum import StrEnum
from typing import Iterable

class OrderType(StrEnum):
ONLINE = "online"
IN_STORE = "in store"

@dataclass
class Item:
name: str
price: Decimal

@dataclass
class Email:
body: str
subject: str
recipient: str
sender: str

@staticmethod
def generate_confirmation_email(order: "Order") -> "Email":
return Email(
body=f"Thank you for your order! Your order #{order.order_id} has been confirmed.",
subject="Order Confirmation",
recipient=order.customer_email,
sender="sales@webshop.com",
)

@staticmethod
def generate_shipping_notification(order: "Order") -> "Email":
return Email(
body=f"Good news! Your order #{order.order_id} has been shipped and is on its way.",
subject="Order Shipped",
recipient=order.customer_email,
sender="sales@webshop.com",
)

@dataclass
class Order:
order_id: int
order_type: OrderType
customer_email: str

def __init__(self, order_id: int, order_type: OrderType, customer_email: str):
self.order_id = order_id
self.order_type = order_type
self.customer_email = customer_email

def process(self) -> None:
if self.order_type == OrderType.ONLINE:
print("Processing online order...")
print(Email.generate_confirmation_email(self))
print("Shipping the order...")
print(Email.generate_shipping_notification(self))
elif self.order_type == OrderType.IN_STORE:
print("Processing in-store order...")
print(Email.generate_confirmation_email(self))
print("Order ready for pickup.")
else:
raise ValueError("Invalid order type.")
print("Order processed successfully.")

def calculate_price(items: Iterable[Item], discount: Decimal = Decimal("0")) -> Decimal:
total_price = Decimal(sum(item.price for item in items))
discounted_price = total_price - (total_price * discount)
return discounted_price

def main() -> None:
items = [
Item(name="T-Shirt", price=Decimal("19.99")),
Item(name="Jeans", price=Decimal("49.99")),
Item(name="Shoes", price=Decimal("79.99")),
]

online_order = Order(
order_id=123, order_type=OrderType.ONLINE, customer_email="sarah@gmail.com"
)

total_price = calculate_price(items, Decimal("0.0"))
print("Total price:", total_price)

discounted_price = calculate_price(items, Decimal("0.1"))
print("Discounted price:", discounted_price)

online_order.process()

in_store_order = Order(
order_id=456, order_type=OrderType.IN_STORE, customer_email="john@gmail.com"
)

in_store_order.process()

if __name__ == "__main__":
main()

REPLY
Andreas [ArjanCodes Team]

Great solution! There are however one smaller remark. When using the @dataclass decorator, you do not need to have a __init__ method. The dataclass does that for us. This does not break the code, but it is redundant

REPLY
Jacky Wong

I love that calculate_price() has a default discount param!

REPLY
Andreas [ArjanCodes Team]

Agreed! It is a very nice addition

REPLY
Anton Prusakov

Why method that sending emails should know anything about order? maybe it's better to create just function with id,body, subject, recipient parameters. also we could re use it for later for example invoice confirmation.

REPLY
Andreas [ArjanCodes Team]

there is a data coupling happening here. Can be argued to change for the id,body,subject and recipient parameters. That would probably make the solution more future proof because of that. However, the current solution does not send the actual emails, just generating them. So, if we were to create a send_email function, then we could make that more flexible.

REPLY
Manuel Escalona

Here is my solution:

from decimal import Decimal
from dataclasses import dataclass
from enum import StrEnum
from typing import Iterable

class OrderType(StrEnum):
ONLINE = "online"
IN_STORE = "in store"

class NotificationType(StrEnum):
CONFIRMATION = "confirmation"
SHIPPING = "shipping"

@dataclass
class Item:
name: str
price: Decimal

@dataclass
class Order:
id: int
type: OrderType
customer_email: str

@dataclass
class Email:
body: str
subject: str
recipient: str
sender: str

def calculate_prices(items: Iterable[Item], discount: Decimal | None) -> dict[str, Decimal]:
list_prices: dict[str, Decimal] = {}
total_price = Decimal(sum(item.price for item in items))
list_prices["total_price"] = total_price
if discount is not None:
discounted_price = total_price - (total_price * discount)
list_prices["discounted_price"] = discounted_price
return list_prices


def generate_order_email_notifications(order: Order, notification: NotificationType) -> Email:
if notification == NotificationType.CONFIRMATION:
body = f"Thank you for your order! Your order #{order.id} has been confirmed."
subject = "Order Confirmation"
elif notification == NotificationType.SHIPPING:
body = f"Good news! Your order #{order.id} has been shipped and is on its way."
subject = "Order Shipped"
return Email(
body = body,
subject = subject,
recipient = order.customer_email,
sender = "sales@webshop.com",
)

def process_orders(order: Order) -> None:
print(f"Processing {order.type} order...")
print(generate_order_email_notifications(order, NotificationType.CONFIRMATION))
if order.type == OrderType.ONLINE:
print("Shipping the order...")
print(generate_order_email_notifications(order, NotificationType.SHIPPING))
elif order.type == OrderType.IN_STORE:
print("Order ready for pickup.")
print("Order processed successfully.")

def main() -> None:
items = [
Item(name="T-Shirt", price=Decimal("19.99")),
Item(name="Jeans", price=Decimal("49.99")),
Item(name="Shoes", price=Decimal("79.99")),
]

online_order = Order(
id=123, type=OrderType.ONLINE, customer_email="sarah@gmail.com"
)

total_price = calculate_prices(items, Decimal("0.1"))
print("Total price:", total_price["total_price"])
print("Discounted price:", total_price["discounted_price"])

process_orders(online_order)

in_store_order = Order(
id=456, type=OrderType.IN_STORE, customer_email="john@gmail.com"
)

process_orders(in_store_order)

if __name__ == "__main__":
main()

REPLY
Andreas [ArjanCodes Team]

Nice solution! There are some minor remarks, like


def process_orders(order: Order) -> None:
print(f"Processing {order.type} order...")
print(generate_order_email_notifications(order, NotificationType.CONFIRMATION))
if order.type == OrderType.ONLINE:
print("Shipping the order...")
print(generate_order_email_notifications(order, NotificationType.SHIPPING))
elif order.type == OrderType.IN_STORE:
print("Order ready for pickup.")
print("Order processed successfully.")

can be changed so we have a dictionary that holds the connection between the function call and order_type. Otherwise, this is a valid solution :)

REPLY
Show More